/****************************************************************************
 * Copyright 2009 Huascar A. Sanchez                                        *
 *                                                                          *
 * Licensed under the Apache License, Version 2.0 (the "License");          *
 * you may not use this file except in compliance with the License.         *
 * You may obtain a copy of the License at                                  *
 *                                                                          *
 *     http://www.apache.org/licenses/LICENSE-2.0                           *
 *                                                                          *
 * Unless required by applicable law or agreed to in writing, software      *
 * distributed under the License is distributed on an "AS IS" BASIS,        *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
 * See the License for the specific language governing permissions and      *
 * limitations under the License.                                           *
 ****************************************************************************/
package com.gotobject.filter.spi;

import static com.gotobject.filter.common.Iterables.asList;
import org.w3c.dom.*;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.StringWriter;
import java.net.URL;
import java.util.List;
import java.util.UUID;

/**
 * @author huascar.sanchez@gmail.com (Huascar A. Sanchez)
 */
public class XmlBuilder {
    private static final URL DOC_WITH_NO_URL = null;
    private final static ThreadLocalWithURL DOC = new ThreadLocalWithURL();

    private XmlBuilder(){
        // do nothing.
    }

    public static Document currentDocument(){
        return DOC.get();
    }

    public static Document currentDocument(URL from){
        return DOC.get(from);
    }

    public static void reset(){
        DOC.set(null);
    }
    
    public static Attr attribute(final String name, final Object value){
        final Attr attr = currentDocument().createAttribute(name);
        attr.setValue(String.valueOf(value));
        return attr;
    }
    
    public static Comment comment(final String comment){
        return currentDocument().createComment(comment);
    }

    public static Element element(final String elemName, Iterable<Element> elements){
        final List<Element> elemList = asList(elements);
        return element(elemName, elemList.toArray(new Element[elemList.size()]));
    }

    public static Element element(final String elmName, final Node... childNodes){
        final Element elm = currentDocument().createElement(elmName);
        for (Node node : childNodes){
            if (node instanceof Attr){
                 if(!elm.getOwnerDocument().equals(node.getOwnerDocument())){
                     final Node copy = currentDocument().importNode(node, true);
                     elm.setAttributeNode((Attr) copy);
                 } else {
                     elm.setAttributeNode((Attr) node);
                 }
            } else {
                if(!elm.getOwnerDocument().equals(node.getOwnerDocument())){
                   elm.appendChild(currentDocument().importNode(node, true));
                } else {
                    elm.appendChild(node);
                }
            }
        }
        return elm;
    }

    public static Element element(final String elmName, final Object value){
        if (value instanceof Node){
            // Ensure that varargs override is invoked when invoked with single node as second argument.
            return element(elmName, new Node[]{(Node) value});
        }
        
        final Element elm = currentDocument().createElement(elmName);
        elm.setTextContent(String.valueOf(value));
        return elm;
    }

    public static Document document(URL from){
        final Document   newDocument    = (Document) ((from == null) ? currentDocument().cloneNode(true) : currentDocument(from).cloneNode(true));
        reset();
        return newDocument;
    }

    public static Document document(){
        return document((URL)null);
    }

    public static Document document(final String version, final boolean standalone){
        final Document d = document();
        d.setXmlVersion(version);
        d.setXmlStandalone(standalone);
        return d;
    }

    public static Document document(final String version){
        final Document d = document();
        d.setXmlVersion(version);
        return d;
    }


   public static Document document(final boolean standalone){
        final Document d = document();
        d.setXmlStandalone(standalone);
        return d;
    }

    public static Document document(final Node rootNode){
        currentDocument().appendChild(rootNode);
        return document();
    }

    public static Document document(final String version, final boolean standalone, final Node rootNode){
        final Document d = document(rootNode);
        d.setXmlVersion(version);
        d.setXmlStandalone(standalone);
        return d;
    }

    public static Document document(final String version, final Node rootNode){
        final Document d = document(rootNode);
        d.setXmlVersion(version);
        return d;
    }

    public static Document document(final boolean standalone, final Node rootNode){
        Document d = document(rootNode);
        d.setXmlStandalone(standalone);
        return d;
    }


    private static Document makeNewDocument() throws ParserConfigurationException {
        final DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        db.setErrorHandler(new XmlDocErrorHandler());
        return db.newDocument();
    }

    public static String output(Document doc){
        try {
            final TransformerFactory tf = TransformerFactory.newInstance();
            final Transformer        t  = tf.newTransformer();
            final DOMSource          ds = new DOMSource(doc);
            final StringWriter       sw = new StringWriter();
            final StreamResult       sr = new StreamResult(sw);

            t.transform(ds, sr);

            return sw.toString();
        } catch (Exception e){
            throw new Error(e);
        }
    }

    private static Document rebuildFromExistingFile(URL path) {
        try {
            final File xmlFile = new File(path.toURI());
            final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setIgnoringElementContentWhitespace(true);
            final DocumentBuilder db = dbf.newDocumentBuilder();
            db.setErrorHandler(new XmlDocErrorHandler());
            final Document d = db.parse(xmlFile);
            d.getDocumentElement().normalize();
            return d;
        } catch (Exception x) {
            throw new IllegalStateException(x);
        }
    }
    
    public static Text text(final Object value){
        Text elm = currentDocument().createTextNode(UUID.randomUUID().toString());
        elm.setTextContent(String.valueOf(value));
        return elm;
    }

    /**
     * helper class wich contained a threadlocal version of an xml document.
     */
    private static class ThreadLocalWithURL extends ThreadLocal<Document> {
        @Override
        public Document get() {
            return get(DOC_WITH_NO_URL);
        }

        // alternative method when having an existing xml file.
        public Document  get(URL path) {
            Document instance = super.get();
            if (instance == null){
                try {
                    instance = (path == null) ? makeNewDocument() : rebuildFromExistingFile(path);
                    super.set(instance);
                } catch (Exception e) {
                    throw new IllegalStateException("Could not create a new Document instance.", e);
                }
            }
            return instance;
        }

    }

    /**
     * error handler for a given {@code xml document}.
     */
    private static class XmlDocErrorHandler implements ErrorHandler {
        public void error(SAXParseException x) throws SAXException {
            throw x;
        }

        public void fatalError(SAXParseException x) throws SAXException {
            throw x;
        }

        public void warning(SAXParseException x) throws SAXException {
            throw x;
        }
    }
}
